Цель - на основе данных пользовательской активность провести RFM сегментацию покупателей для дальнейшей корректировки маркетинговых событий.
Описание данных:
Датасет описывает транзакции интернет-магазина товаров для дома и быта «Пока все ещё тут».
Колонки в /datasets/ecom_dataset_upd.csv :
Ход исследования:
import pandas as pd
import datetime as dt
import plotly.express as px
import matplotlib.pyplot as plt
import warnings
warnings.simplefilter("ignore", category=FutureWarning)
import nltk
from nltk.corpus import stopwords
from pymystem3 import Mystem
from string import punctuation
nltk.download('stopwords')
[nltk_data] Downloading package stopwords to /home/jovyan/nltk_data... [nltk_data] Package stopwords is already up-to-date!
True
import scipy.stats
from scipy import stats as st
from scipy.stats import binom, norm
df = pd.read_csv('/datasets/ecom_dataset_upd.csv')
def data_full_info(dataset):
print (dataset.info())
print('\033[1m' + 'Количество дубликатов в таблице:' + '\033[0m', dataset.duplicated().sum())
return (dataset.head(10))
data_full_info(df)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7474 entries, 0 to 7473
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 date 7474 non-null int64
1 customer_id 7474 non-null object
2 order_id 7474 non-null int64
3 product 7474 non-null object
4 quantity 7474 non-null int64
5 price 7474 non-null float64
dtypes: float64(1), int64(3), object(2)
memory usage: 350.5+ KB
None
Количество дубликатов в таблице: 0
| date | customer_id | order_id | product | quantity | price | |
|---|---|---|---|---|---|---|
| 0 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | Комнатное растение в горшке Алое Вера, d12, h30 | 1 | 142.0 |
| 1 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | Комнатное растение в горшке Кофе Арабика, d12,... | 1 | 194.0 |
| 2 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | Радермахера d-12 см h-20 см | 1 | 112.0 |
| 3 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | Хризолидокарпус Лутесценс d-9 см | 1 | 179.0 |
| 4 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | Циперус Зумула d-12 см h-25 см | 1 | 112.0 |
| 5 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | Шеффлера Лузеана d-9 см | 1 | 164.0 |
| 6 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | Юкка нитчатая d-12 см h-25-35 см | 1 | 134.0 |
| 7 | 2018100108 | 375e0724-f033-4c76-b579-84969cf38ee2 | 68479 | Настенная сушилка для белья Gimi Brio Super 100 | 1 | 824.0 |
| 8 | 2018100108 | 6644e5b4-9934-4863-9778-aaa125207701 | 68478 | Таз пластмассовый 21,0 л круглый "Водолей" С61... | 1 | 269.0 |
| 9 | 2018100109 | c971fb21-d54c-4134-938f-16b62ee86d3b | 68480 | Чехол для гладильной доски Colombo Persia Beig... | 1 | 674.0 |
df['product'].nunique()
2343
Датасет содержит в себе сведения о покупках в интернет-магазине товаров для дома. После изучения общей информации о датасете видно, что пропуски и явные дубликаты в записях отсутствуют, однако необходимо провести проверку данных на наличие неявных дубликатов. Кроме того, нужно будет изменить типы данных в таблице и добавить несколько новых столбов для полного исследования.
Предобработка названий товаров, что может выявить дубликаты.
df['product'] = df['product'].str.lower()
df['product'] = df['product'].str.replace('ё', 'е', regex=True)
df.duplicated().sum()
0
Явных дубликатов нет.
Проверка неявных дубликатов, не рассматривая столбец с датой.
df_duplicated = df[df.duplicated(['customer_id', 'order_id', 'product', 'quantity', 'price'])]
df_duplicated
| date | customer_id | order_id | product | quantity | price | |
|---|---|---|---|---|---|---|
| 58 | 2018100218 | b731df05-98fa-4610-8496-716ec530a02c | 68474 | доска гладильная eurogold professional 130х48 ... | 1 | 3299.0 |
| 59 | 2018100219 | b731df05-98fa-4610-8496-716ec530a02c | 68474 | доска гладильная eurogold professional 130х48 ... | 1 | 3299.0 |
| 60 | 2018100220 | b731df05-98fa-4610-8496-716ec530a02c | 68474 | доска гладильная eurogold professional 130х48 ... | 1 | 3299.0 |
| 63 | 2018100304 | b731df05-98fa-4610-8496-716ec530a02c | 68474 | доска гладильная eurogold professional 130х48 ... | 1 | 3299.0 |
| 94 | 2018100413 | 32de7df8-8d4f-4c84-a7b9-c41d00dd83ba | 68522 | эвкалипт гунни d-17 см h-60 см | 1 | 1409.0 |
| ... | ... | ... | ... | ... | ... | ... |
| 6706 | 2019102810 | 57cc80a2-2610-4eef-9457-e7c3bf0c72f0 | 70960 | сумка-тележка 2-х колесная gimi argo красная | 1 | 1087.0 |
| 6711 | 2019102821 | cb65d08a-dae7-4890-aef0-bb9f79055e02 | 73108 | мирт d-9 см h-15 см | 1 | 134.0 |
| 6728 | 2019103100 | ffaeab76-3a8d-49ee-860f-17273b2fc8a2 | 73136 | таз пластмассовый со стиральной доской (иж), 1... | 1 | 397.0 |
| 6729 | 2019103102 | ffaeab76-3a8d-49ee-860f-17273b2fc8a2 | 73136 | таз пластмассовый со стиральной доской (иж), 1... | 1 | 397.0 |
| 6736 | 2019103116 | 344aa778-e436-419e-a9c6-9b8f37b7c1df | 73137 | сумка-тележка 2-х колесная gimi argo синяя | 1 | 1087.0 |
1864 rows × 6 columns
percent = (len(df_duplicated) / len(df))
print('Процент дубликатов от общего числа записей:', format(percent, ".00%"))
Процент дубликатов от общего числа записей: 25%
Несмотря на то, что четверть датафрейма составляют неяные дубликаты, нам необходимо их удалить, так как их наличие может сильно повредить анализу.
df = df.drop_duplicates(subset=['customer_id', 'order_id', 'product', 'quantity', 'price'])
df = df.reset_index(drop=True)
Проверка неявных дубликатов на наличие заказов совершенных более чем одним покупателем.
two_customers_order = df.groupby('order_id')['customer_id'].nunique().reset_index()
two_customers_order.sort_values(by='customer_id', ascending=False)
two_customers_order = two_customers_order.query('customer_id == 1')
one_co = two_customers_order['order_id']
df = df.query('order_id in @one_co').reset_index(drop=True)
df
| date | customer_id | order_id | product | quantity | price | |
|---|---|---|---|---|---|---|
| 0 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | комнатное растение в горшке алое вера, d12, h30 | 1 | 142.0 |
| 1 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | комнатное растение в горшке кофе арабика, d12,... | 1 | 194.0 |
| 2 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | радермахера d-12 см h-20 см | 1 | 112.0 |
| 3 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | хризолидокарпус лутесценс d-9 см | 1 | 179.0 |
| 4 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | циперус зумула d-12 см h-25 см | 1 | 112.0 |
| ... | ... | ... | ... | ... | ... | ... |
| 5539 | 2020013021 | 63208953-a8e4-4f77-9b47-3a46e7b72eee | 104002 | томата (помидор) черниченский черри № 116 сорт... | 2 | 38.0 |
| 5540 | 2020013022 | d99d25f1-4017-4fcd-8d29-c580cc695a1a | 107336 | дендробиум санок анна грин 1 ствол d-12 см | 1 | 869.0 |
| 5541 | 2020013102 | 2c9bd08d-8c55-4e7a-9bfb-8c56ba42c6d6 | 106336 | подставка для обуви резиновая attribute 80x40 ... | 1 | 354.0 |
| 5542 | 2020013112 | cdd17932-623e-415f-a577-3b31312fd0e2 | 102002 | тагетис крупноцветковый рассада однолетних цве... | 1 | 128.0 |
| 5543 | 2020013115 | 2e460a26-35af-453d-a369-a036e95a40e0 | 103225 | вешалка для блузок 41 см красный attribute ahm781 | 1 | 104.0 |
5544 rows × 6 columns
df['date'] = pd.to_datetime(df['date'], format='%Y%m%d%H')
Добавление столбцов с датами
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
#df['date_month'] = df['date'].astype('datetime64[M]')
df["year_month"] = df.date.dt.to_period('M').dt.to_timestamp()
Добавление столбца с суммой купленного товара
df['total_price'] = df['price'] * df['quantity']
df
| date | customer_id | order_id | product | quantity | price | year | month | year_month | total_price | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | комнатное растение в горшке алое вера, d12, h30 | 1 | 142.0 | 2018 | 10 | 2018-10-01 | 142.0 |
| 1 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | комнатное растение в горшке кофе арабика, d12,... | 1 | 194.0 | 2018 | 10 | 2018-10-01 | 194.0 |
| 2 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | радермахера d-12 см h-20 см | 1 | 112.0 | 2018 | 10 | 2018-10-01 | 112.0 |
| 3 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | хризолидокарпус лутесценс d-9 см | 1 | 179.0 | 2018 | 10 | 2018-10-01 | 179.0 |
| 4 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | циперус зумула d-12 см h-25 см | 1 | 112.0 | 2018 | 10 | 2018-10-01 | 112.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 5539 | 2020-01-30 21:00:00 | 63208953-a8e4-4f77-9b47-3a46e7b72eee | 104002 | томата (помидор) черниченский черри № 116 сорт... | 2 | 38.0 | 2020 | 1 | 2020-01-01 | 76.0 |
| 5540 | 2020-01-30 22:00:00 | d99d25f1-4017-4fcd-8d29-c580cc695a1a | 107336 | дендробиум санок анна грин 1 ствол d-12 см | 1 | 869.0 | 2020 | 1 | 2020-01-01 | 869.0 |
| 5541 | 2020-01-31 02:00:00 | 2c9bd08d-8c55-4e7a-9bfb-8c56ba42c6d6 | 106336 | подставка для обуви резиновая attribute 80x40 ... | 1 | 354.0 | 2020 | 1 | 2020-01-01 | 354.0 |
| 5542 | 2020-01-31 12:00:00 | cdd17932-623e-415f-a577-3b31312fd0e2 | 102002 | тагетис крупноцветковый рассада однолетних цве... | 1 | 128.0 | 2020 | 1 | 2020-01-01 | 128.0 |
| 5543 | 2020-01-31 15:00:00 | 2e460a26-35af-453d-a369-a036e95a40e0 | 103225 | вешалка для блузок 41 см красный attribute ahm781 | 1 | 104.0 | 2020 | 1 | 2020-01-01 | 104.0 |
5544 rows × 10 columns
В ходе предобработки данных были выявлены и удалены неявные дубликаты, изменены типы данных и добавлены дополнительные столбцы. Преобработка данных завершена.
print('Общее количество проданного товара за исследуемый период составляет:', df['quantity'].sum())
Общее количество проданного товара за исследуемый период составляет: 14314
print('Общее количество совершенных заказов за исследуемый период составляет:',df['order_id'].nunique())
Общее количество совершенных заказов за исследуемый период составляет: 3492
print('Общее количество покупателей за исследуемый период составляет:',df['customer_id'].nunique())
Общее количество покупателей за исследуемый период составляет: 2413
order_per_customer = df.groupby('customer_id')['order_id'].nunique().reset_index()
order_per_customer.describe()
| order_id | |
|---|---|
| count | 2413.000000 |
| mean | 1.447161 |
| std | 2.698082 |
| min | 1.000000 |
| 25% | 1.000000 |
| 50% | 1.000000 |
| 75% | 2.000000 |
| max | 126.000000 |
order_per_customer
| customer_id | order_id | |
|---|---|---|
| 0 | 000d6849-084e-4d9f-ac03-37174eaf60c4 | 1 |
| 1 | 001cee7f-0b29-4716-b202-0042213ab038 | 1 |
| 2 | 00299f34-5385-4d13-9aea-c80b81658e1b | 1 |
| 3 | 002d4d3a-4a59-406b-86ec-c3314357e498 | 1 |
| 4 | 003bbd39-0000-41ff-b7f9-2ddaec152037 | 1 |
| ... | ... | ... |
| 2408 | ff601403-b094-4b86-9ac6-264d725b9277 | 2 |
| 2409 | ffaeab76-3a8d-49ee-860f-17273b2fc8a2 | 1 |
| 2410 | ffb5976a-7a4d-460b-95c4-5ffaba31cb24 | 1 |
| 2411 | ffb80538-3fda-4351-8ea9-9d2bec58bb07 | 1 |
| 2412 | ffe82299-3f5b-4214-87fe-3d36ecccfac3 | 1 |
2413 rows × 2 columns
fig = px.box(order_per_customer.query('order_id <= 20'), y='order_id')
fig.update_layout(title='Распределение количества заказов на человека',
yaxis_title='Количество заказов')
fig.show()
В среднем на одного человека приходится 1 покупка. Максимальное количество совершенных одним человеком покупок составляет 126 покупок, что является выбрасом в данных. Для дальнейшего исследования стоит оставить показатели меньше 20единиц.
order_per_customer= order_per_customer.query('order_id <= 20')
cust_id = order_per_customer['customer_id']
df_new = df.query('customer_id in @cust_id')
df_new
| date | customer_id | order_id | product | quantity | price | year | month | year_month | total_price | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | комнатное растение в горшке алое вера, d12, h30 | 1 | 142.0 | 2018 | 10 | 2018-10-01 | 142.0 |
| 1 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | комнатное растение в горшке кофе арабика, d12,... | 1 | 194.0 | 2018 | 10 | 2018-10-01 | 194.0 |
| 2 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | радермахера d-12 см h-20 см | 1 | 112.0 | 2018 | 10 | 2018-10-01 | 112.0 |
| 3 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | хризолидокарпус лутесценс d-9 см | 1 | 179.0 | 2018 | 10 | 2018-10-01 | 179.0 |
| 4 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | циперус зумула d-12 см h-25 см | 1 | 112.0 | 2018 | 10 | 2018-10-01 | 112.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 5539 | 2020-01-30 21:00:00 | 63208953-a8e4-4f77-9b47-3a46e7b72eee | 104002 | томата (помидор) черниченский черри № 116 сорт... | 2 | 38.0 | 2020 | 1 | 2020-01-01 | 76.0 |
| 5540 | 2020-01-30 22:00:00 | d99d25f1-4017-4fcd-8d29-c580cc695a1a | 107336 | дендробиум санок анна грин 1 ствол d-12 см | 1 | 869.0 | 2020 | 1 | 2020-01-01 | 869.0 |
| 5541 | 2020-01-31 02:00:00 | 2c9bd08d-8c55-4e7a-9bfb-8c56ba42c6d6 | 106336 | подставка для обуви резиновая attribute 80x40 ... | 1 | 354.0 | 2020 | 1 | 2020-01-01 | 354.0 |
| 5542 | 2020-01-31 12:00:00 | cdd17932-623e-415f-a577-3b31312fd0e2 | 102002 | тагетис крупноцветковый рассада однолетних цве... | 1 | 128.0 | 2020 | 1 | 2020-01-01 | 128.0 |
| 5543 | 2020-01-31 15:00:00 | 2e460a26-35af-453d-a369-a036e95a40e0 | 103225 | вешалка для блузок 41 см красный attribute ahm781 | 1 | 104.0 | 2020 | 1 | 2020-01-01 | 104.0 |
5356 rows × 10 columns
price = df_new.groupby('order_id')['total_price'].sum().reset_index()
price
| order_id | total_price | |
|---|---|---|
| 0 | 12624 | 375.0 |
| 1 | 13547 | 684.0 |
| 2 | 14480 | 359.0 |
| 3 | 14481 | 600.0 |
| 4 | 14482 | 376.0 |
| ... | ... | ... |
| 3326 | 112722 | 450.0 |
| 3327 | 112732 | 90.0 |
| 3328 | 112767 | 38.0 |
| 3329 | 112779 | 172.0 |
| 3330 | 112789 | 2698.0 |
3331 rows × 2 columns
fig = px.box(price.query('total_price <= 20000'), y='total_price')
fig.update_layout(title='Распределение стоимости покупки',
yaxis_title='Стоимость')
fig.show()
В данных имеется слишком большое количество выбросов по стоимости заказа. Такие показатели могут указыывать на оптовые покупки, возможно не физискими лицами, а для коммерческого использования. Показатели более 20000 стоит исключить из данных.
price = price.query('total_price <= 20000')
price_oi = price['order_id']
df_new = df_new.query('order_id in @price_oi')
mean_receipt = df_new.groupby('order_id')['total_price'].mean().reset_index()
mean_receipt.describe()
| order_id | total_price | |
|---|---|---|
| count | 3324.000000 | 3324.000000 |
| mean | 73817.249398 | 935.585923 |
| std | 23978.422962 | 1444.091668 |
| min | 12624.000000 | 9.000000 |
| 25% | 69577.750000 | 149.000000 |
| 50% | 71319.500000 | 442.000000 |
| 75% | 72975.250000 | 1087.000000 |
| max | 112789.000000 | 16536.000000 |
fig = px.box(mean_receipt, y='total_price')
fig.update_layout(title='Стоимость среднего чека без выбросов',
yaxis_title='Стоимость')
fig.show()
В среднем стоимость чека одного заказа составляет 935р. Однако медианное значение составляет 442р.
print('Дата первого заказа в датасете:', df_new['date'].min())
print('Дата последнего заказа в датасете:', df_new['date'].max())
print('Период исследования:', df_new['date'].max() - df_new['date'].min())
Дата первого заказа в датасете: 2018-10-01 00:00:00 Дата последнего заказа в датасете: 2020-01-31 15:00:00 Период исследования: 487 days 15:00:00
order_per_day = (df_new.pivot_table(index='year_month', values='total_price', aggfunc={'sum', 'mean'})
.sort_values(by='year_month', ascending=False)
.reset_index())
order_per_day.columns = ['date', 'mean', 'sum']
order_per_day['mean'] = round(order_per_day['mean'], 2)
fig = px.bar(order_per_day,
template='plotly_white',
x='date',
y='sum'
)
fig.update_layout(title='Сумма покупок в динамике',
xaxis_title='Дата',
yaxis_title='Сумма покупок')
fig.update_layout(showlegend=False)
fig.show()
fig = px.bar(order_per_day,
template='plotly_white',
x='date',
y='mean'
)
fig.update_layout(title='Средний чек в динамике',
xaxis_title='Дата',
yaxis_title='Средний чек')
fig.update_layout(showlegend=False)
fig.show()
df_order_count = df_new.pivot_table(index='year_month', values='quantity', aggfunc='sum')
fig = px.line(df_order_count, markers=True)
fig.update_layout(title='Количество купленных товаров в динамике',
xaxis_title='Дата',
yaxis_title='Количество товаров',
template='plotly_white')
fig.update_layout(showlegend=False)
fig.show()
До середины весны 2019г. продажи шли достаточно равномерно, но был провал в январе 2019г., возможно, в следствие продолжительных зимних выходных.
В апреле 2019г. был пик продаж за весь период исследования (выручка более 316тыс.р.).
После данной отметки продажи резко пошли на спад.
Рассматривая средний чек покупки, можно увидеть в некоторых периодых обратно пропорциональную зависимость. Так при низкой доходности зимой 2018-2019г. средний чек был в 2 раза выше, чем в самый прибыльный период.
Проведение предобработки текстовых данных, приведение к начальной формы слова, для облегчения категоризации.
mystem = Mystem()
russian_stopwords = stopwords.words("russian")
russian_stopwords.extend(['лента','ассорт','разм','арт','что', 'это', 'так', 'вот', 'быть', 'как', 'в', '—', 'к', 'на'])
def preprocess_text(text):
text = str(text)
tokens = mystem.lemmatize(text.lower())
tokens = [token for token in tokens if token not in russian_stopwords\
and token != " " \
and len(token)>=3 \
and token.strip() not in punctuation \
and token.isdigit()==False]
text = " ".join(tokens)
return text
Проверка работы кода
print('Было:', df_new['product'][0])
print('Стало:', preprocess_text(df['product'][0]))
Было: комнатное растение в горшке алое вера, d12, h30 Стало: комнатный растение горшок алый вера d12 h30
df_new['processed']=df_new['product'].apply(preprocess_text)
df_new.head()
/tmp/ipykernel_31/1481695642.py:1: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
| date | customer_id | order_id | product | quantity | price | year | month | year_month | total_price | processed | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2018-10-01 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | комнатное растение в горшке алое вера, d12, h30 | 1 | 142.0 | 2018 | 10 | 2018-10-01 | 142.0 | комнатный растение горшок алый вера d12 h30 |
| 1 | 2018-10-01 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | комнатное растение в горшке кофе арабика, d12,... | 1 | 194.0 | 2018 | 10 | 2018-10-01 | 194.0 | комнатный растение горшок кофе арабика d12 h25 |
| 2 | 2018-10-01 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | радермахера d-12 см h-20 см | 1 | 112.0 | 2018 | 10 | 2018-10-01 | 112.0 | радермахер |
| 3 | 2018-10-01 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | хризолидокарпус лутесценс d-9 см | 1 | 179.0 | 2018 | 10 | 2018-10-01 | 179.0 | хризолидокарпус лутесценс |
| 4 | 2018-10-01 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | циперус зумула d-12 см h-25 см | 1 | 112.0 | 2018 | 10 | 2018-10-01 | 112.0 | циперус зумул |
Создание списков слов, по которым будет проиходить категоризация
plants = ['аспарагус','афеляндра','альбука', 'артемизия','аптения','алоэ', 'арбуз', 'аммигум', 'бакопа',
'базилик', 'барвинок', 'бегония', 'бальзамин', 'вербейник', 'вербена', 'герань', 'гербера', 'гвоздика',
'гиностемма', 'годеция', 'дендробиум', 'дифенбахия', 'дыня', 'ель', 'ежеплодник', 'ежеголовник', 'евпомация',
'еремагона', 'зев','зверобой','земляника','зимовник','иссоп','калибрахоа','кашпо','клубника', 'камнеломка',
'колокольчик', 'капуста', 'кипарис', 'клоффера', 'калатея', 'каланхое', 'калоцефалус', 'капсикум', 'календула',
'кореопсис','космея', 'лобелия', 'лапнатка', 'микс', 'мирт', 'молодило', 'нолина', 'осина', 'овсянница', 'папоротник',
'пеларгония', 'петрушка', 'примула', 'паунсентия', 'роза', 'радермахера', 'растение', 'смесь', 'соланум', 'томат',
'фиалка', 'флокс', 'фикус', 'фаленопсис', 'хризалидокарпус', 'хлорофитум', 'хризантема', 'цикламен', 'цветок',
'циперус', 'цикламен', 'радермахер', 'хризолидокарпус', 'шеффлер', 'юкка', 'эхеверия', 'мята', 'лавр', 'декабрист',
'чабер', 'гиацинт', 'джункус', 'драцена', 'фатсия', 'лапчатка', 'петуния', 'портулак', 'эхеверие', 'эвкалипт',
'адиантум', 'кампануть', 'глоклиния', 'азалия', 'кориандр', 'сантолина', 'антуриум', 'душица', 'мелисса', 'розмарин',
'альбук', 'лаванда', 'каллун', 'мимоза', 'бархатцы', 'косметь', 'морковь', 'настурция', 'огурец', 'гвоздик', 'цинерария',
'подсолнечник', 'аптение', 'пуансеттия', 'крассула', 'скиммия', 'ориттоний', 'тимьян', 'спатифиллум', 'кофе',
'эхинокактус', 'фал', 'крокус', 'крокус', 'гипсофил', 'тюльпан', 'эпипремнум', 'горох', 'укроп', 'нефролепис', 'калла',
'нарцисс', 'ранункулус', 'смолевка', 'цинния', 'котовник', 'осколка', 'виола', 'платикодон', 'энотера', 'фуксия',
'цитрофортунелла', 'глоксиния', 'фиттоний', 'георгин', 'ясколка', 'пеперомия', 'синнингия', 'гардения', 'виноград',
'мускарь', 'пиретрум', 'муррайя', 'баклажан', 'патиссон', 'гайлардий', 'монард', 'нивянник', 'рудбекия', 'седум',
'молодить', 'аллисум', 'бузульник', 'солидаго', 'физостегия', 'бадан', 'лен', 'лантан', 'лаватер', 'анемон',
'буддлей', 'валериана', 'змееголовник', 'любисток', 'табак', 'шалфей', 'вигна', 'кабачок', 'тыква', 'хамедорей',
'мединилла', 'клен', 'замиокулькас', 'амариллис', 'салат', 'пахира', 'пиретрум', 'алиссум', 'эшшольция']
seedlings = ['рассада', 'черенок']
textile = ['плед', 'скатерть', 'полотенце', 'салфетка', 'штора', 'наматрасник', 'покрывало', 'простынь', 'белье', 'подушка',
'одеяло', 'наволочка', 'халат', 'пододеяльник', 'простыня', 'ткань', 'наматрацник', 'тряпка', 'пуф', 'завертка']
decor = ['муляж', 'ковер', 'коврик', 'фоторамка', 'искусственный', 'декоративный']
for_kitchen = ['сковорода', 'тарелка', 'ложка', 'вилка', 'нож', 'котел', 'фужер', 'кувшин', 'толкушка', 'чайник', 'банка',
'крышка', 'миксер', 'электроштопор', 'посуда', 'антипригарный', 'рыбочистка', 'овощеварка', 'салатник',
'мантоварка', 'кисточка', 'кружка', 'пресс', 'термостакан', 'кухонный', 'скалка', 'противень', 'чайный', 'терка',
'салфетница', 'стакан', 'кондитерский', 'картофелемялка', 'кекс', 'миска', 'бокал', 'столовый', 'термокружок',
'термос', 'разделочный', 'блюдце', 'бульонница', 'кастрюля', 'хлебница', 'форма', 'венчик', 'орехоколка',
'сито', 'тортница', 'блюдо', 'сотейник', 'сахарница', 'соковарка', 'соковыжималка', 'половник',
'кипятильник']
bags = ['сумка', 'тележка']
for_cleaning = ['таз', 'вантуз', 'корыто', 'щетка', 'ерш', 'перчатки', 'швабра', 'губка', 'чистка', 'антижир', 'совок', 'сметка',
'лоток', 'перчатка', 'веник', 'средство', 'ролик', 'ковш', 'мытье', 'пылесос', 'полировка', 'окно', 'окномойка']
for_washing = ['сушилка', 'гладильный', 'стиральный', 'пятна', 'крем', 'одежда', 'подрукавник', 'кондиционер',
'соль', 'гель', 'прищепка', 'утюг', 'прищепок', 'сетка', 'стирка']
for_bathroom = ['дозатор', 'штанга', 'ванный', 'ванна', 'пробка', 'фен', 'мыльница', 'ванна', 'подголовник', 'шпагат']
tools = ['шило', 'термометр', 'инструмент', 'сверел', 'сверла', 'сверло', 'напильник', 'стремянка', 'стяжк', 'линейка',
'шпингалет', 'сварка', 'крепеж', 'пружин', 'шнур', 'весы', 'бензин', 'лопатка', 'петля', 'светильник',
'петля', 'скоба', 'штангенциркуль', 'насадка', 'фиксатор', 'батарейка', 'решетка', 'уголок', 'свереть']
storage = ['вешалка', 'чехол', 'корзина', 'контейнер', 'кофр', 'карниз', 'ящик', 'хранение', 'коробка', 'урна', 'комод',
'полка', 'ключница', 'подставка', 'плечики', 'ведро', 'емкость', 'крючок', 'полк', 'обувница', 'стеллаж', 'бидон',
'этажерка']
hygiene = ['зубной', 'туалетный', 'пена', 'бумага', 'мыло', 'маска']
Функция автоматичекой категоризации
df_new['category'] = 0
def categoryzer(list_of_words, category):
join = '|'.join(list_of_words)
index = df_new[df_new['processed'].str.lower().str.contains(join)].index.to_list()
for i in index:
df_new.loc[i, 'category'] = category
return df_new
/tmp/ipykernel_31/2080708419.py:1: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
categoryzer(plants, 'Растения')
categoryzer(bags, 'Сумки и тележки')
categoryzer(storage, 'Хранение')
categoryzer(textile, 'Текстиль')
categoryzer(decor, 'Декор')
categoryzer(for_kitchen, 'Для кухни')
categoryzer(for_cleaning, 'Для уборки')
categoryzer(for_washing, 'Для стирки')
categoryzer(hygiene, 'Гигиена')
categoryzer(tools, 'Инструменты')
categoryzer(seedlings, 'Рассада')
categoryzer(for_bathroom, 'Для ванной')
/opt/conda/lib/python3.9/site-packages/pandas/core/indexing.py:1720: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
| date | customer_id | order_id | product | quantity | price | year | month | year_month | total_price | processed | category | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | комнатное растение в горшке алое вера, d12, h30 | 1 | 142.0 | 2018 | 10 | 2018-10-01 | 142.0 | комнатный растение горшок алый вера d12 h30 | Растения |
| 1 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | комнатное растение в горшке кофе арабика, d12,... | 1 | 194.0 | 2018 | 10 | 2018-10-01 | 194.0 | комнатный растение горшок кофе арабика d12 h25 | Растения |
| 2 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | радермахера d-12 см h-20 см | 1 | 112.0 | 2018 | 10 | 2018-10-01 | 112.0 | радермахер | Растения |
| 3 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | хризолидокарпус лутесценс d-9 см | 1 | 179.0 | 2018 | 10 | 2018-10-01 | 179.0 | хризолидокарпус лутесценс | Растения |
| 4 | 2018-10-01 00:00:00 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | циперус зумула d-12 см h-25 см | 1 | 112.0 | 2018 | 10 | 2018-10-01 | 112.0 | циперус зумул | Растения |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 5539 | 2020-01-30 21:00:00 | 63208953-a8e4-4f77-9b47-3a46e7b72eee | 104002 | томата (помидор) черниченский черри № 116 сорт... | 2 | 38.0 | 2020 | 1 | 2020-01-01 | 76.0 | томат помидор черниченский черри сорт индетерм... | Растения |
| 5540 | 2020-01-30 22:00:00 | d99d25f1-4017-4fcd-8d29-c580cc695a1a | 107336 | дендробиум санок анна грин 1 ствол d-12 см | 1 | 869.0 | 2020 | 1 | 2020-01-01 | 869.0 | дендробиум санки анна грин ствол | Растения |
| 5541 | 2020-01-31 02:00:00 | 2c9bd08d-8c55-4e7a-9bfb-8c56ba42c6d6 | 106336 | подставка для обуви резиновая attribute 80x40 ... | 1 | 354.0 | 2020 | 1 | 2020-01-01 | 354.0 | подставка обувь резиновый attribute 80x40 amc080 | Хранение |
| 5542 | 2020-01-31 12:00:00 | cdd17932-623e-415f-a577-3b31312fd0e2 | 102002 | тагетис крупноцветковый рассада однолетних цве... | 1 | 128.0 | 2020 | 1 | 2020-01-01 | 128.0 | тагетис крупноцветковый рассада однолетний цве... | Рассада |
| 5543 | 2020-01-31 15:00:00 | 2e460a26-35af-453d-a369-a036e95a40e0 | 103225 | вешалка для блузок 41 см красный attribute ahm781 | 1 | 104.0 | 2020 | 1 | 2020-01-01 | 104.0 | вешалка блузка красный attribute ahm781 | Хранение |
5324 rows × 12 columns
category_total = df_new.groupby('category')['total_price'].sum().reset_index()
category_total['total_price'] = round(category_total['total_price'])
category_total.sort_values(by='total_price', ascending=False).reset_index(drop=True)
| category | total_price | |
|---|---|---|
| 0 | Сумки и тележки | 737082.0 |
| 1 | Для стирки | 687303.0 |
| 2 | Растения | 412735.0 |
| 3 | Хранение | 264116.0 |
| 4 | Для кухни | 233997.0 |
| 5 | Декор | 218565.0 |
| 6 | Текстиль | 217203.0 |
| 7 | Инструменты | 193515.0 |
| 8 | Для ванной | 185611.0 |
| 9 | Для уборки | 178966.0 |
| 10 | Рассада | 158180.0 |
| 11 | Гигиена | 12579.0 |
Категоризация данных выполнена.
Создание функций для визуализации данных
def pie_category(data, name):
category = data.groupby('category')['quantity'].sum().reset_index()
fig = px.pie(category.sort_values(by='quantity', ascending=False), values='quantity', names='category')
fig.update_layout(title='Соотношение популярности категорий товаров ' + name)
return fig.show()
def bar_sum_price(data, name):
df_category = (data.groupby(['year_month', 'category'])['total_price']
.sum()
.reset_index())
df_category.columns = ['date', 'category', 'sum_price']
df_category['sum_price'] = round(df_category['sum_price'])
df_category = df_category.sort_values(by='sum_price', ascending=False)
fig = px.bar(df_category,
x='date',
y='sum_price',
template='plotly_white',
color='category',
labels=dict(category="Категория"))
fig.update_layout(width=1000,
height=700)
fig.update_layout(title='Сумма покупок по категориям ' + name,
xaxis_title='Дата',
yaxis_title='Сумма покупок')
return fig.show()
def box_receipt(data, name):
mean_receipt = data.groupby(['category','order_id'])['total_price'].mean().reset_index()
fig = px.box(mean_receipt.query('total_price <= 12000'),
y="total_price",
x="category",
color="category",
template='plotly_white',
labels={"category": "Категории товаров"})
fig.update_layout(title='Распределение суммы покупки по категориям товаров ' + name,
xaxis_title='Категория',
yaxis_title='Сумма покупки')
fig.update_layout(showlegend=False)
return fig.show()
def line_count_products(data, name):
df_category_count = data.groupby(['year_month', 'category'])['quantity'].count().reset_index()
df_category_count.columns = ['date', 'category', 'count']
fig = px.line(df_category_count,
x="date", y="count",
template='plotly_white',
color='category',
labels=dict(category="Категория"))
fig.update_layout(title='Количество купленных товаров по категориям ' + name,
xaxis_title='Дата',
yaxis_title='Количество товаров')
return fig.show()
pie_category(df_new, 'общих данных')
Самыми популярными категориями товаров за весь исследуемый период являются:
Мешьне всего товар продано из категорий гигиена, для ванной и сумки и тележки.
box_receipt(df_new, 'общих данных')
Средний чек одного заказа самый высокий в категории "Инструменты". Самый низкий у категории "Рассада".
bar_sum_price(df_new, 'общих данных')
В самый прибыльный месяц (июнь 2019г) больше всего принесли товары категории "сумки и тележки" (около 73тыс.р.).
Весной 2019г. большую сумму для магазина составили купленные товары категорий растения, рассада и товары для стирки.
Товары для стирки, а также сумки и тележки в весь исследуемый период приносили высокий доход.
line_count_products(df_new, 'общих данных')
Больше всего товаров по количеству было продано из категорий растения и рассада весной 2019г, что объясняется началом дачно-огородного сезона. Несмотря на то, что это самые популярные товары в данный период, их стоимость невысока, поэтому прибыль с них не так заметна.
Примечательно, что товары для кухни чаще продаются в ноябре, январе и марте. Возможно, это связано с зимними или женскими праздниками(день матери и 8 марта).
Инструменты чаще продаются осенью и зимой.
Сумки и тележки пользуются популярностью в декабре, апреле и августе. Есть вероятность, что товары данной категории также предпочитают в качестве подарка.
Продажи текстиля проваливаются в весенне-летний период.
print('Общее количество проданного товара за исследуемый период без выбросов составляет:', df_new['quantity'].sum())
print('Общее количество совершенных заказов за исследуемый период без выбросов составляет:',df_new['order_id'].nunique())
print('Общее количество покупателей за исследуемый период без выбросов составляет:',df_new['customer_id'].nunique())
Общее количество проданного товара за исследуемый период без выбросов составляет: 12100 Общее количество совершенных заказов за исследуемый период без выбросов составляет: 3324 Общее количество покупателей за исследуемый период без выбросов составляет: 2405
Таким образом, за исследуемый период с октября 2018г. по январь 2020г. было совершено 3324 покупок и продано 12100 товаров. Самым прибыльным периодом оказался апрель 2019г. До этого момента продажи шли равномерно, однако после пошли на спад и не догнали показатели зимы-весны 2019г.
Всего категорий товаров - 12:
Самые популярные категории товаров - растения, декор, рассада.
Больше всего в прибыльный период было продано товаров категории растения и рассада, но самые дорогостоящие товары относились к категории сумки и тележки и растения.
Самый большой средний чек у категории товаров "инструменты"
NOW = dt.datetime(2020,1,31)
RFM_data = df_new
RFM_data['date'] = pd.to_datetime(RFM_data['date'])
# RFM таблица
RFM_table=RFM_data.groupby('customer_id').agg({'date': lambda x: (NOW - x.max()).days, # Давность
'order_id': lambda x: len(x.unique()), # Частота
'total_price': lambda x: x.sum()}) # Денежная ценность
RFM_table['date'] = RFM_table['date'].astype(int)
RFM_table.rename(columns={'date': 'recency',
'order_id': 'frequency',
'total_price': 'monetary_value'}, inplace=True)
RFM_table.head()
/tmp/ipykernel_31/862126640.py:3: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
| recency | frequency | monetary_value | |
|---|---|---|---|
| customer_id | |||
| 000d6849-084e-4d9f-ac03-37174eaf60c4 | 106 | 1 | 555.0 |
| 001cee7f-0b29-4716-b202-0042213ab038 | 348 | 1 | 442.0 |
| 00299f34-5385-4d13-9aea-c80b81658e1b | 108 | 1 | 914.0 |
| 002d4d3a-4a59-406b-86ec-c3314357e498 | 368 | 1 | 1649.0 |
| 003bbd39-0000-41ff-b7f9-2ddaec152037 | 123 | 1 | 2324.0 |
квинтили — составление четырех равных частей на основе доступных значений — для расчета показателя RFM.
quantiles = RFM_table.quantile(q=[0.25,0.5,0.75])
quantiles
| recency | frequency | monetary_value | |
|---|---|---|---|
| 0.25 | 71.0 | 1.0 | 389.0 |
| 0.50 | 205.0 | 1.0 | 832.0 |
| 0.75 | 342.0 | 2.0 | 1798.0 |
# Преобразование квантилей в словарь, более простое в использовании
quantiles = quantiles.to_dict()
## Сегментация RFM ----
RFM_Segment = RFM_table.copy()
# Аргументы (x = value, p = recency, monetary_value, frequency, k = quartiles dict)
def R_Class(x,p,d):
if x <= d[p][0.25]:
return 4
elif x <= d[p][0.50]:
return 3
elif x <= d[p][0.75]:
return 2
else:
return 1
# Аргументы (x = value, p = recency, monetary_value, frequency, k = quartiles dict)
def FM_Class(x,p,d):
if x <= d[p][0.25]:
return 1
elif x <= d[p][0.50]:
return 2
elif x <= d[p][0.75]:
return 3
else:
return 4
RFM_Segment['R_Quartile'] = RFM_Segment['recency'].apply(R_Class, args=('recency',quantiles,))
RFM_Segment['F_Quartile'] = RFM_Segment['frequency'].apply(FM_Class, args=('frequency',quantiles,))
RFM_Segment['M_Quartile'] = RFM_Segment['monetary_value'].apply(FM_Class, args=('monetary_value',quantiles,))
RFM_Segment['RFMClass'] = RFM_Segment.R_Quartile.map(str) \
+ RFM_Segment.F_Quartile.map(str) \
+ RFM_Segment.M_Quartile.map(str)
Идеальные покупатели
RFM_Segment[RFM_Segment['RFMClass']=='444'].sort_values('monetary_value', ascending=False).head()
| recency | frequency | monetary_value | R_Quartile | F_Quartile | M_Quartile | RFMClass | |
|---|---|---|---|---|---|---|---|
| customer_id |
Покупателей с классом 444 не найдено
Лучшие покупатели
top_customers = RFM_Segment[RFM_Segment['RFMClass']=='344'].sort_values('monetary_value', ascending=False).reset_index()
top_customers.columns = ['customer_id', 'recency', 'frequency', 'monetary_value', 'r_quartile', 'f_quartile', 'm_quartile', 'rfm_class']
top_customers
| customer_id | recency | frequency | monetary_value | r_quartile | f_quartile | m_quartile | rfm_class | |
|---|---|---|---|---|---|---|---|---|
| 0 | 73d1cd35-5e5f-4629-8cf2-3fda829d4e58 | 91 | 17 | 21361.0 | 3 | 4 | 4 | 344 |
| 1 | d02429ab-22e0-4ff2-9465-3082befde444 | 158 | 3 | 3261.0 | 3 | 4 | 4 | 344 |
| 2 | 6a86cc77-ef15-496f-b5d3-89005597ee5d | 153 | 3 | 3151.0 | 3 | 4 | 4 | 344 |
| 3 | e8204583-4d55-4724-ad3f-049c7db43bdd | 146 | 3 | 3102.0 | 3 | 4 | 4 | 344 |
Топ-покупателей мало (4 человека), они делали заказы недавно, часто и принесли достаточно прибыли
Преданные покупатели
loyal_customers = RFM_Segment[RFM_Segment['F_Quartile'] >= 3 ].sort_values('monetary_value', ascending=False).reset_index()
loyal_customers.columns = ['customer_id', 'recency', 'frequency', 'monetary_value', 'r_quartile', 'f_quartile', 'm_quartile', 'rfm_class']
loyal_customers
| customer_id | recency | frequency | monetary_value | r_quartile | f_quartile | m_quartile | rfm_class | |
|---|---|---|---|---|---|---|---|---|
| 0 | 498f12a4-6a62-4725-8516-cf5dc9ab8a3a | 286 | 4 | 41900.0 | 2 | 4 | 4 | 244 |
| 1 | 73d1cd35-5e5f-4629-8cf2-3fda829d4e58 | 91 | 17 | 21361.0 | 3 | 4 | 4 | 344 |
| 2 | 940c175f-ea87-44e0-9e16-0a3d0a9abecd | 232 | 2 | 20232.0 | 2 | 3 | 4 | 234 |
| 3 | f279d50f-a508-40b4-bde5-5cb4a1be3ad0 | 30 | 2 | 16557.0 | 4 | 3 | 4 | 434 |
| 4 | ad66d870-22f5-43bc-958f-73420822586b | 67 | 2 | 13731.0 | 4 | 3 | 4 | 434 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 865 | 07bde29d-a3b1-4d72-9f37-ad5a52b07f25 | 6 | 2 | 112.0 | 4 | 3 | 1 | 431 |
| 866 | 2f639db6-6ce7-426b-9a6d-edbb9025463c | 145 | 2 | 95.0 | 3 | 3 | 1 | 331 |
| 867 | 43ff529f-8ff0-473d-8168-4b730137a2e1 | 3 | 2 | 94.0 | 4 | 3 | 1 | 431 |
| 868 | ae036d1b-b1d5-41e4-8c85-d2bd1b0b4e18 | 3 | 2 | 88.0 | 4 | 3 | 1 | 431 |
| 869 | aa7151ae-8da0-41ca-85b6-5c1b331d5bdc | 22 | 2 | 84.0 | 4 | 3 | 1 | 431 |
870 rows × 8 columns
Преданных покупателей насчитывается 870 человек. Они делают заказ довольно часто, приносят большую прибыль, но могли совершить покупку давно.
Клиенты на пороге оттока
lowvalue_customers = RFM_Segment[RFM_Segment['R_Quartile'] <= 2 ].sort_values('monetary_value', ascending=False).reset_index()
lowvalue_customers.columns = ['customer_id', 'recency', 'frequency', 'monetary_value', 'r_quartile', 'f_quartile', 'm_quartile', 'rfm_class']
lowvalue_customers
| customer_id | recency | frequency | monetary_value | r_quartile | f_quartile | m_quartile | rfm_class | |
|---|---|---|---|---|---|---|---|---|
| 0 | 498f12a4-6a62-4725-8516-cf5dc9ab8a3a | 286 | 4 | 41900.0 | 2 | 4 | 4 | 244 |
| 1 | 940c175f-ea87-44e0-9e16-0a3d0a9abecd | 232 | 2 | 20232.0 | 2 | 3 | 4 | 234 |
| 2 | 909564b8-3a5c-4d3e-8310-5ba1c837bbd7 | 360 | 1 | 16536.0 | 1 | 1 | 4 | 114 |
| 3 | 5d189e88-d4d6-4eac-ab43-fa65a3c4d106 | 255 | 1 | 15300.0 | 2 | 1 | 4 | 214 |
| 4 | 639c4989-b0ab-412a-b7ec-be394cb2d372 | 451 | 3 | 12095.0 | 1 | 4 | 4 | 144 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1197 | 48788184-498b-49da-955e-7737bf7c9047 | 282 | 1 | 22.0 | 2 | 1 | 1 | 211 |
| 1198 | d5660bff-407f-463d-b2ff-e90a9ca05976 | 288 | 1 | 22.0 | 2 | 1 | 1 | 211 |
| 1199 | 2b6439c9-1ae1-4785-9509-ca4348b3d39a | 276 | 1 | 22.0 | 2 | 1 | 1 | 211 |
| 1200 | 21df03ad-d9ac-47e9-a482-5372fd3e2464 | 302 | 1 | 22.0 | 2 | 1 | 1 | 211 |
| 1201 | 2330d859-e9cb-4c8f-abd0-55f9e27e6745 | 443 | 1 | 15.0 | 1 | 1 | 1 | 111 |
1202 rows × 8 columns
Таких клиентов на февраль 2020г. насчитывается уже 1202 человека. В последний раз они совершили покупку давно и больше не возвращаются в магазин, хотя могли сделать достаточно дорогостоящий заказ.
Потерянные клиенты
lost_customers = RFM_Segment[RFM_Segment['RFMClass']=='111'].sort_values('recency',ascending=False).reset_index()
lost_customers.columns = ['customer_id', 'recency', 'frequency', 'monetary_value', 'r_quartile', 'f_quartile', 'm_quartile', 'rfm_class']
lost_customers
| customer_id | recency | frequency | monetary_value | r_quartile | f_quartile | m_quartile | rfm_class | |
|---|---|---|---|---|---|---|---|---|
| 0 | 5f75c73a-1305-4079-b040-2bcb42085002 | 485 | 1 | 299.0 | 1 | 1 | 1 | 111 |
| 1 | 32a85453-f14d-40c2-90ba-3851498a5f3b | 485 | 1 | 374.0 | 1 | 1 | 1 | 111 |
| 2 | f08d9018-438e-4e96-b519-f74c0302a433 | 485 | 1 | 359.0 | 1 | 1 | 1 | 111 |
| 3 | 29a514f5-a27e-4939-85d5-874a0c3f7a2c | 485 | 1 | 193.0 | 1 | 1 | 1 | 111 |
| 4 | eee7b1fa-ba81-4049-add6-370ee5e62e72 | 484 | 1 | 187.0 | 1 | 1 | 1 | 111 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 145 | 384c1e4b-ac85-44bf-8968-4377d626e983 | 344 | 1 | 134.0 | 1 | 1 | 1 | 111 |
| 146 | d220a441-2ed1-428e-8f3f-966beb50bd0e | 344 | 1 | 314.0 | 1 | 1 | 1 | 111 |
| 147 | d4972bf5-827f-4777-8dd0-05f4f95fe10c | 343 | 1 | 209.0 | 1 | 1 | 1 | 111 |
| 148 | 1142023b-a2ee-4e31-b6ef-d5de7a527892 | 343 | 1 | 120.0 | 1 | 1 | 1 | 111 |
| 149 | 01ca3206-da26-470d-8240-f34e7086c214 | 343 | 1 | 157.0 | 1 | 1 | 1 | 111 |
150 rows × 8 columns
Безвозвратно потерянными можно считать 150 клиентов. Последний заказ они совершили очень давно, и чаще всего это была единоразовая покупка.
df_merged = df_new.merge(RFM_Segment, on='customer_id')
fig = px.scatter(df_merged.sort_values(by='RFMClass'), x="RFMClass", y="monetary_value")
fig.update_layout(title='Распределение денежной значимости по классам',
xaxis_title='Класс',
yaxis_title='Денежная значимость')
fig.show()
Зависимость между сегментом и денежной значимость не выявлена.
fig = px.scatter(df_merged.sort_values(by='RFMClass'), x="RFMClass", y="recency")
fig.update_layout(title='Распределение давности покупки по классам',
xaxis_title='Класс',
yaxis_title='Давность')
fig.show()
Чем ниже класс, тем более равние покупки были совершены.
fig = px.scatter(df_merged.sort_values(by='RFMClass'), x="RFMClass", y="frequency")
fig.update_layout(title='Распределение Частоты покупки по классам',
xaxis_title='Класс',
yaxis_title='Частота')
fig.show()
Лояльные клиенты немного чаще других совершают покупки. Клиенты класса "на пороге оттока" с значением 244 приближаются к перемещению в сегмент лояльных клиентов.
fig = px.scatter_matrix(df_merged,
dimensions=["quantity", "price", "total_price", "recency", "frequency", "monetary_value"]) # remove underscore
fig.update_layout(title='Матрица корреляции',
width=1000,
height=600
)
fig.update_traces(diagonal_visible=False)
fig.show()
По матрице корреляции видно прямая зависимость между ценой продукта и общего чека заказа, а также между денежной ценностью, которую несет покупатель, и ценой продукта и общим чеком.
Топ-покупатели
top_customers_id = top_customers['customer_id']
tc_df = df_new.query('customer_id in @top_customers_id')
tc_df.describe()
| order_id | quantity | price | year | month | total_price | |
|---|---|---|---|---|---|---|
| count | 39.000000 | 39.000000 | 39.000000 | 39.0 | 39.000000 | 39.000000 |
| mean | 49037.794872 | 1.256410 | 763.564103 | 2019.0 | 8.025641 | 791.666667 |
| std | 28905.155099 | 0.715172 | 1160.755081 | 0.0 | 2.032463 | 1147.183102 |
| min | 14763.000000 | 1.000000 | 82.000000 | 2019.0 | 5.000000 | 97.000000 |
| 25% | 14856.000000 | 1.000000 | 128.000000 | 2019.0 | 6.000000 | 175.500000 |
| 50% | 72400.000000 | 1.000000 | 397.000000 | 2019.0 | 8.000000 | 397.000000 |
| 75% | 72984.500000 | 1.000000 | 1034.000000 | 2019.0 | 10.000000 | 1034.000000 |
| max | 73164.000000 | 4.000000 | 6600.000000 | 2019.0 | 10.000000 | 6600.000000 |
pie_category(tc_df, 'для Топ-клиентов')
box_receipt(tc_df, 'для Топ-клиентов')
bar_sum_price(tc_df, 'для Топ-клиентов')
line_count_products(tc_df, 'для Топ-клиентов')
В сегменте "Топ-покупатели" самыми популярными были категории: растения, сумки и тележки, для стирки.
Самая дорогостоящая покупка была категория сумки и тележки, которая принесла также больше всего прибыли осенью 2019г.
Больше всего товаров было куплено в июне 2019г., категория - растения.
После октября 2019г. покупатели данного сегмента не возвращались в магазин.
Преданные покупатели
loyal_customers_id = loyal_customers['customer_id']
lc_df = df_new.query('customer_id in @loyal_customers_id')
lc_df.describe()
| order_id | quantity | price | year | month | total_price | |
|---|---|---|---|---|---|---|
| count | 2133.000000 | 2133.000000 | 2133.000000 | 2133.000000 | 2133.000000 | 2133.000000 |
| mean | 72917.586967 | 1.401782 | 692.140920 | 2019.002813 | 6.567745 | 762.971832 |
| std | 31888.042710 | 2.434380 | 1130.203504 | 0.490999 | 4.053434 | 1312.972801 |
| min | 14481.000000 | 1.000000 | 9.000000 | 2018.000000 | 1.000000 | 9.000000 |
| 25% | 69172.000000 | 1.000000 | 112.000000 | 2019.000000 | 3.000000 | 120.000000 |
| 50% | 71510.000000 | 1.000000 | 194.000000 | 2019.000000 | 6.000000 | 239.000000 |
| 75% | 104345.000000 | 1.000000 | 734.000000 | 2019.000000 | 11.000000 | 779.000000 |
| max | 112789.000000 | 60.000000 | 8437.000000 | 2020.000000 | 12.000000 | 15680.000000 |
pie_category(lc_df, 'для Преданных клиентов')
box_receipt(lc_df, 'для Преданных клиентов')
bar_sum_price(lc_df, 'для Преданных клиентов')
line_count_products(lc_df, 'для Преданных клиентов')
В сегменте "Преданные покупатели" самыми популярными категориями были: растения, рассада и для стирки.
Самые дорогостоящие покупки являлись заказы категорий текстиль, сумки и тележки, для стирки.
Больше всего покупатели потратились на сумки и тележки в зимний период (особенно декабрь 2019г.) и весной 2019г. Текстиль активно покупался в январе и апреле 2019 г. Товары для стирки также были довольно прибыльной категорий товаров преимущественно в зимний период.
Больше всего по количеству товаров было куплено в апреое 2019г. категории растения и рассада, а также в январе 2020г. категории растения.
Клиенты на пороге оттока
lowvalue_customers_id = lowvalue_customers['customer_id']
lvc_df = df_new.query('customer_id in @lowvalue_customers_id')
lvc_df.describe()
| order_id | quantity | price | year | month | total_price | |
|---|---|---|---|---|---|---|
| count | 3193.000000 | 3193.000000 | 3193.000000 | 3193.000000 | 3193.00000 | 3193.000000 |
| mean | 39566.752584 | 2.721578 | 337.221109 | 2018.727529 | 5.95678 | 516.379894 |
| std | 27453.801935 | 10.190932 | 700.959604 | 0.445301 | 3.24964 | 1111.781945 |
| min | 12624.000000 | 1.000000 | 9.000000 | 2018.000000 | 1.00000 | 9.000000 |
| 25% | 14698.000000 | 1.000000 | 82.000000 | 2018.000000 | 4.00000 | 112.000000 |
| 50% | 14848.000000 | 1.000000 | 135.000000 | 2019.000000 | 5.00000 | 150.000000 |
| 75% | 69332.000000 | 2.000000 | 210.000000 | 2019.000000 | 10.00000 | 442.000000 |
| max | 71907.000000 | 300.000000 | 8737.000000 | 2019.000000 | 12.00000 | 16536.000000 |
pie_category(lvc_df, 'для Клиентов на пороге оттока')
box_receipt(lvc_df, 'для Клиентов на пороге оттока')
bar_sum_price(lvc_df, 'для Клиентов на пороге оттока')
line_count_products(lvc_df, 'для Клиентов на пороге оттока')
В сегменте "Клиенты на пороге оттока" Самыми популярными были товары категорий: растения, декор, рассада.
Самыми дорогостоящими покупками были заказы сумок и тележек, растений и товаров для стирки.
Самый высокий средний чек у категории инструменты.
В осенне-зимний период лучше продаются и приносят прибыль сумки и тележки, товары для стирки и декор. В весенний - растения, рассада, сумки и тележки и текстиль.
За весь период самые часто продаваемые категории - растения и рассада.
Последняя покупка клиентов данного сегмента была совершена в июле 2019г.
Потерянные клиенты
lost_customers_id = lost_customers['customer_id']
lostc_df = df_new.query('customer_id in @lost_customers_id')
lostc_df.describe()
| order_id | quantity | price | year | month | total_price | |
|---|---|---|---|---|---|---|
| count | 179.000000 | 179.000000 | 179.000000 | 179.000000 | 179.000000 | 179.000000 |
| mean | 63525.871508 | 2.027933 | 136.743017 | 2018.385475 | 7.363128 | 183.418994 |
| std | 16970.859989 | 2.380705 | 97.063635 | 0.488073 | 4.663627 | 102.569231 |
| min | 14480.000000 | 1.000000 | 15.000000 | 2018.000000 | 1.000000 | 15.000000 |
| 25% | 68773.000000 | 1.000000 | 59.000000 | 2018.000000 | 2.000000 | 104.000000 |
| 50% | 69262.000000 | 1.000000 | 119.000000 | 2018.000000 | 10.000000 | 156.000000 |
| 75% | 69859.000000 | 2.000000 | 188.000000 | 2019.000000 | 11.000000 | 261.000000 |
| max | 70387.000000 | 15.000000 | 389.000000 | 2019.000000 | 12.000000 | 389.000000 |
pie_category(lostc_df, 'для Потерянных клиентов')
box_receipt(lostc_df, 'для Потерянных клиентов')
bar_sum_price(lostc_df, 'для Потерянных клиентов')
line_count_products(lostc_df, 'для Потерянных клиентов')
В сегменте "Потерянные клиенты" самыми популярными были категории: декор, растения и товары для кухни.
Самый высокий средний чек был у заказа товаров категории текстиль.
Прибыльными категориями товаров были растения зимой 2018-2019г. и товары ванной в октябре 2018г.
Больше всего по количеству товаров было куплено растений, товаров для кухни и декора(все в период с декабря 2018г. по февраль 2019г.)
Последний заказ клиентов этого сегмента был совершен в феврале 2019г.
После проведения RFM-анализа мы смогли поделить пользователей на 4 сегмента - топ-клиенты или лучший покупатели, лояльные клиенты, покупатели на пороге оттока и потерянные клиенты.
Сегменты клиентов интернет-магазина имеют весомые для анализа и дальнейших рекомендаций отличия.
Нагляднее всего данные о лояльных клиентах и о тех, кто подвержен риску оттока.
Н0 - статистически значимая разница между средними чеками кластеров покупателей отсутствует
Н1 - статистически значимая разница между средними чеками кластеров покупателей присутствует
from scipy.stats import shapiro
import scipy.stats as stats
# Определение функции для теста Шапиро-Уилка
def shapiro_test(data, alpha=0.05):
stat, p = shapiro(data)
if p > alpha:
print('Данные выглядят как нормальные (гауссовские)')
else:
print('Данные не выглядят как нормальные (гауссовские)')
shapiro_test(tc_df['total_price'])
shapiro_test(lc_df['total_price'])
shapiro_test(lostc_df['total_price'])
shapiro_test(lvc_df['total_price'])
Данные не выглядят как нормальные (гауссовские) Данные не выглядят как нормальные (гауссовские) Данные не выглядят как нормальные (гауссовские) Данные не выглядят как нормальные (гауссовские)
shapiro_test(top_customers['frequency'])
shapiro_test(loyal_customers['frequency'])
shapiro_test(lowvalue_customers['frequency'])
shapiro_test(lost_customers['frequency'])
Данные не выглядят как нормальные (гауссовские) Данные не выглядят как нормальные (гауссовские) Данные не выглядят как нормальные (гауссовские) Данные выглядят как нормальные (гауссовские)
/opt/conda/lib/python3.9/site-packages/scipy/stats/_morestats.py:1797: UserWarning: Input data for shapiro has range zero. The results may not be accurate.
def h_test(sample_1, sample_2, alpha):
results = st.mannwhitneyu(sample_1, sample_2)
if results.pvalue < alpha:
print('Отвергаем нулевую гипотезу: разница статистически значима')
else:
print('Не получилось отвергнуть нулевую гипотезу, вывод о различии сделать нельзя')
return print('p-значение: ', results.pvalue)
start = "\033[1m"
end = "\033[0;0m"
print(start + 'Проверка топ-клиентов и лояльных клиентов' + end)
h_test(tc_df['total_price'], lc_df['total_price'], 0.05)
print(start + 'Проверка топ-клиентов и клиентов на пороге оттока' + end)
h_test(tc_df['total_price'], lvc_df['total_price'], 0.05)
print(start + 'Проверка топ-клиентов и потерянных клиентов' + end)
h_test(tc_df['total_price'], lostc_df['total_price'], 0.05)
print(start + 'Проверка лояльных клиентов и клиентов на пороге оттока' + end)
h_test(lc_df['total_price'], lvc_df['total_price'], 0.05)
print(start + 'Проверка лояльных клиентов и потерянных клиентов' + end)
h_test(lc_df['total_price'], lostc_df['total_price'], 0.05)
print(start + 'Проверка клиентов на пороге оттока и потерянных клиентов' + end)
h_test(lvc_df['total_price'], lostc_df['total_price'], 0.05)
Проверка топ-клиентов и лояльных клиентов Не получилось отвергнуть нулевую гипотезу, вывод о различии сделать нельзя p-значение: 0.09424317103407745 Проверка топ-клиентов и клиентов на пороге оттока Отвергаем нулевую гипотезу: разница статистически значима p-значение: 0.00016343030901591079 Проверка топ-клиентов и потерянных клиентов Отвергаем нулевую гипотезу: разница статистически значима p-значение: 2.780304589747676e-08 Проверка лояльных клиентов и клиентов на пороге оттока Отвергаем нулевую гипотезу: разница статистически значима p-значение: 1.6934614412087197e-23 Проверка лояльных клиентов и потерянных клиентов Отвергаем нулевую гипотезу: разница статистически значима p-значение: 1.412745799988815e-11 Проверка клиентов на пороге оттока и потерянных клиентов Отвергаем нулевую гипотезу: разница статистически значима p-значение: 0.018913000690952204
data = pd.DataFrame({'test': ['one', 'two', 'three', 'four', 'five', 'six'],
'p_value': [0.09424317103407745,
0.00016343030901591079,
2.780304589747676e-08,
1.6934614412087197e-23,
1.412745799988815e-11,
0.018913000690952204]})
#столбец alpha значениями 0.05
data['alpha'] = 0.05
# столбец test_result будет содержать True, если p-value меньше alpha
data['test_result'] = data['p_value'] < data['alpha']
m = data['test'].count()# число тестов здесь
alpha = 0.05#уровень значимости здесь
result = []
#цикл для расчёта коррекции уровня значимости
for i in range(m):
result += [alpha / (m-i)]
data['alpha_holm'] = pd.Series(result) # результат в столбец
data['test_result_corr'] = data['p_value'] < data['alpha_holm']# сравнение p-value со скорректированным уровнем значимости
print(data[['test', 'p_value', 'alpha', 'test_result', 'alpha_holm', 'test_result_corr']])
print('Доли отвергнутых Η0 для тестов без коррекции и с коррекцией:',
data['test_result'].mean(),
data['test_result_corr'].mean()
)
test p_value alpha test_result alpha_holm test_result_corr 0 one 9.424317e-02 0.05 False 0.008333 False 1 two 1.634303e-04 0.05 True 0.010000 True 2 three 2.780305e-08 0.05 True 0.012500 True 3 four 1.693461e-23 0.05 True 0.016667 True 4 five 1.412746e-11 0.05 True 0.025000 True 5 six 1.891300e-02 0.05 True 0.050000 True Доли отвергнутых Η0 для тестов без коррекции и с коррекцией: 0.8333333333333334 0.8333333333333334
В случае сравнения топ-клиентов с лояльными клиентами мы не можем отвергнуть нулевую гипотезу о том, что средний чек купленных товаров не имеет статистически значимых отличий. Следовательно, разница между остальными сегментами по среднему чеку покупок товаров статистически значима.
Н0 - статистически значимая разница между частотой покупок между кластерами покупателей отсутствует
Н1 - статистически значимая разница между частотой покупок между кластерами покупателей присутствует
print(start + 'Проверка топ-клиентов и лояльных клиентов' + end)
h_test(top_customers['frequency'], loyal_customers['frequency'], 0.05)
print(start + 'Проверка топ-клиентов и клиентов на пороге оттока' + end)
h_test(top_customers['frequency'], lowvalue_customers['frequency'], 0.05)
print(start + 'Проверка топ-клиентов и потерянных клиентов' + end)
h_test(top_customers['frequency'], lost_customers['frequency'], 0.05)
print(start + 'Проверка лояльных клиентов и клиентов на пороге оттока' + end)
h_test(loyal_customers['frequency'], lowvalue_customers['frequency'], 0.05)
print(start + 'Проверка лояльных клиентов и потерянных клиентов' + end)
h_test(loyal_customers['frequency'], lost_customers['frequency'], 0.05)
print(start + 'Проверка клиентов на пороге оттока и потерянных клиентов' + end)
h_test(lowvalue_customers['frequency'], lost_customers['frequency'], 0.05)
Проверка топ-клиентов и лояльных клиентов Отвергаем нулевую гипотезу: разница статистически значима p-значение: 1.856771038935137e-26 Проверка топ-клиентов и клиентов на пороге оттока Отвергаем нулевую гипотезу: разница статистически значима p-значение: 1.0905380121265699e-11 Проверка топ-клиентов и потерянных клиентов Отвергаем нулевую гипотезу: разница статистически значима p-значение: 4.999983901485529e-35 Проверка лояльных клиентов и клиентов на пороге оттока Отвергаем нулевую гипотезу: разница статистически значима p-значение: 0.0 Проверка лояльных клиентов и потерянных клиентов Отвергаем нулевую гипотезу: разница статистически значима p-значение: 8.235839013402999e-196 Проверка клиентов на пороге оттока и потерянных клиентов Отвергаем нулевую гипотезу: разница статистически значима p-значение: 0.0001421592876252509
data = pd.DataFrame({'test': ['one', 'two', 'three', 'four', 'five', 'six'],
'p_value': [1.856771038935137e-26,
1.0905380121265699e-11,
4.999983901485529e-35,
0.0,
8.235839013402999e-196,
0.0001421592876252509]})
#столбец alpha значениями 0.05
data['alpha'] = 0.05
# столбец test_result будет содержать True, если p-value меньше alpha
data['test_result'] = data['p_value'] < data['alpha']
m = data['test'].count()# число тестов здесь
alpha = 0.05#уровень значимости здесь
result = []
#цикл для расчёта коррекции уровня значимости
for i in range(m):
result += [alpha / (m-i)]
data['alpha_holm'] = pd.Series(result) # результат в столбец
data['test_result_corr'] = data['p_value'] < data['alpha_holm']# сравнение p-value со скорректированным уровнем значимости
print(data[['test', 'p_value', 'alpha', 'test_result', 'alpha_holm', 'test_result_corr']])
print('Доли отвергнутых Η0 для тестов без коррекции и с коррекцией:',
data['test_result'].mean(),
data['test_result_corr'].mean()
)
test p_value alpha test_result alpha_holm test_result_corr 0 one 1.856771e-26 0.05 True 0.008333 True 1 two 1.090538e-11 0.05 True 0.010000 True 2 three 4.999984e-35 0.05 True 0.012500 True 3 four 0.000000e+00 0.05 True 0.016667 True 4 five 8.235839e-196 0.05 True 0.025000 True 5 six 1.421593e-04 0.05 True 0.050000 True Доли отвергнутых Η0 для тестов без коррекции и с коррекцией: 1.0 1.0
По частоте совершения покупок между кластерами покупателей присутствует статистически значимая разница
В ходе исследования было выполнено:
В качестве основной аудитории для проведения рассылок стоит взять сегменты покупателей "Лояльные клиенты" с rfm-классом выше 332 и "На пороге оттока".
Лояльные клиенты потенциально могут стать лучшими клиентами по показателям частоты, давности и денежной ценности покупки. Категории для рекламы стоит брать сумки и тележки, товары для кухни и декор. Категория сумки и тележки прибыльна и популярна, но объемы продаж можно увеличить за счет рекламы. Товары для кухни следует продвигать с ноября по январь. Декор довольно популярен, особенно в период перед предстоящими сезонными праздниками.
"Клиентов на пороге оттока" можно вернуть в ряды лояльных клиентов, обратив внимание на товары категории декор, товары для кухни или для стирки, а также текстиль. Декор и товары для кухни также обладают сезонностью связанной преимущественно с концом осени, новым годом и весной(мартом). Текстиль довольно популярен осенью и в декабре.
На потерянных клиентов не стоит тратить бюджет, так как последняя покупка была совершена год назад от актулальной даты исследования.
В качестве основной аудитории для проведения рассылок стоит взять сегменты покупателей "Лояльные клиенты" с rfm-классом выше 332 и "На пороге оттока".
Лояльные клиенты потенциально могут стать лучшими клиентами по показателям частоты, давности и денежной ценности покупки. Категории для рекламы стоит брать текстиль, декор и сумки и тележки. Данные категории довольно популярны, особенно в период перед предстоящими сезонными праздниками.
"Клиентов на пороге оттока" можно вернуть в ряды лояльных клиентов, обратив внимание на товары категории декор и товары для кухни или для стирки. Декор и товары для кухни также обладают сезонностью связанной преимущественно с концом осени, новым годом и весной(мартом).
На потерянных клиентов не стоит тратить бюджет, так как последняя покупка была совершена год назад от актулальной даты исследования.
Презентация по итогам исследования: https://docs.google.com/presentation/d/1-IWy5yol-D7IVXt1p6TanSXgV6o1QtXWvsLNPtkSDdI/edit?usp=sharing